package com.vdimitr.happycube.puzzle.solution;

import com.vdimitr.happycube.puzzle.matrix.MatrixUtils;
import com.vdimitr.happycube.puzzle.piece.PuzzlePiece;
import com.vdimitr.happycube.puzzle.piece.construction.PuzzlePieceTransformer;

/**
 * This class is responsible for checking that the proposed solution is valid by
 * checking that each side of the cube doesn't contain any holes. For each side
 * it gathers 5 puzzle pieces (top, left, middle, right, bottom) and checks that
 * the matrix created from the matrices of these 5 is compact (full of 1s).
 * These 5 puzzle pieces then form a cross. </br>
 * 
 * <pre>
 *      | top  |
 * |left|middle|right|
 *      |bottom|
 * </pre>
 * 
 * @author vasileios.dimitriadis
 */
public class PuzzlePieceSolutionCrossCheck {

	/**
	 * The left and right puzzle pieces derived from the solution need to be
	 * rotated every time a cross is formed
	 */
	private PuzzlePieceTransformer transformer = new PuzzlePieceTransformer();

	/**
	 * Left puzzle piece
	 */
	private PuzzlePiece left = null;

	/**
	 * Middle puzzle piece
	 */
	private PuzzlePiece middle = null;

	/**
	 * Right puzzle piece
	 */
	private PuzzlePiece right = null;

	/**
	 * Top puzzle piece
	 */
	private PuzzlePiece top = null;

	/**
	 * Bottom puzzle piece
	 */
	private PuzzlePiece bottom = null;

	// <--- begin of setters & getters for puzzle pieces --->
	/**
	 * 
	 * @return
	 */
	public PuzzlePiece getLeftPuzzlePiece() {
		return left;
	}

	/**
	 * 
	 * @param leftPuzzlePiece
	 */
	public void setLeftPuzzlePiece(PuzzlePiece leftPuzzlePiece) {
		this.left = leftPuzzlePiece;
	}

	/**
	 * 
	 * @return
	 */
	public PuzzlePiece getMiddlePuzzlePiece() {
		return middle;
	}

	/**
	 * 
	 * @param middlePuzzlePiece
	 */
	public void setMiddlePuzzlePiece(PuzzlePiece middlePuzzlePiece) {
		this.middle = middlePuzzlePiece;
	}

	/**
	 * 
	 * @return
	 */
	public PuzzlePiece getRightPuzzlePiece() {
		return right;
	}

	/**
	 * 
	 * @param rightPuzzlePiece
	 */
	public void setRightPuzzlePiece(PuzzlePiece rightPuzzlePiece) {
		this.right = rightPuzzlePiece;
	}

	/**
	 * 
	 * @return
	 */
	public PuzzlePiece getTopPuzzlePiece() {
		return top;
	}

	/**
	 * 
	 * @param topPuzzlePiece
	 */
	public void setTopPuzzlePiece(PuzzlePiece topPuzzlePiece) {
		this.top = topPuzzlePiece;
	}

	/**
	 * 
	 * @return
	 */
	public PuzzlePiece getBottomPuzzlePiece() {
		return bottom;
	}

	/**
	 * 
	 * @param bottomPuzzlePiece
	 */
	public void setBottomPuzzlePiece(PuzzlePiece bottomPuzzlePiece) {
		this.bottom = bottomPuzzlePiece;
	}

	// >--- end of setters & getters for puzzle pieces ---<

	/**
	 * Validates a constructed solution
	 * 
	 * @param solution
	 *            a constructed cube solution
	 * @return true if all sides of the cube don't contain any holes
	 */
	public boolean isValid(PuzzlePieceSolution solution) {
		int numberOfCrossCheckings = solution.getMiddlePuzzlePieceColumn().length;
		for (int crossCheckingCount = 0; crossCheckingCount < numberOfCrossCheckings; crossCheckingCount++) {
			setPuzzlePieces(solution, crossCheckingCount);
			if (!isMiddleMatrixFull()) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Sets the puzzle pieces in their right place to make a cross
	 * 
	 * @param solution
	 *            the input puzzle piece solution to retrieve puzzle pieces from
	 * @param crossCheckingCount
	 *            the number of cross to check
	 */
	private void setPuzzlePieces(PuzzlePieceSolution solution,
			int crossCheckingCount) {
		setLeftAndRightPuzzlePieces(solution, crossCheckingCount);
		setMiddleCollumnPuzzlePieces(solution, crossCheckingCount);
	}

	/**
	 * Set left and right rotating them as needed to match their position
	 * 
	 * @param solution
	 *            the input puzzle piece solution to retrieve puzzle pieces from
	 * @param crossCheckingCount
	 *            the number of cross to check
	 */
	private void setLeftAndRightPuzzlePieces(PuzzlePieceSolution solution,
			int crossCheckingCount) {
		PuzzlePiece left = transformer.rotateCounterClockwise(
				solution.getPuzzlePieceAtPosition(5), crossCheckingCount);
		setLeftPuzzlePiece(left);

		PuzzlePiece right = transformer.rotateClockwise(
				solution.getPuzzlePieceAtPosition(6), crossCheckingCount);
		setRightPuzzlePiece(right);
	}

	/**
	 * Set top, middle, bottom
	 * 
	 * @param solution
	 *            the input puzzle piece solution to retrieve puzzle pieces from
	 * @param crossCheckingCount
	 *            the number of cross to check
	 */
	private void setMiddleCollumnPuzzlePieces(PuzzlePieceSolution solution,
			int crossCheckingCount) {
		PuzzlePiece top = solution
				.getPuzzlePieceAtPosition((3 + crossCheckingCount) % 4 + 1);
		PuzzlePiece middle = solution
				.getPuzzlePieceAtPosition((3 + crossCheckingCount + 1) % 4 + 1);
		PuzzlePiece bottom = solution
				.getPuzzlePieceAtPosition((3 + crossCheckingCount + 2) % 4 + 1);

		setTopPuzzlePiece(top);
		setMiddlePuzzlePiece(middle);
		setBottomPuzzlePiece(bottom);
	}

	/**
	 * Check that the 5 puzzle pieces form a concrete side of the cube
	 * 
	 * @return true if there are no holes (i.e. the matrix created by combining
	 *         all 5 puzzle pieces does not contain 0s)
	 */
	private boolean isMiddleMatrixFull() {
		int[][] middleMatrix = getMiddleMatrix(middle);
		addTopSide(top.getMatrix(), middleMatrix);
		addBottomSide(bottom.getMatrix(), middleMatrix);
		addLeftSide(left.getMatrix(), middleMatrix);
		addRightSide(right.getMatrix(), middleMatrix);
		return MatrixUtils.isMatrixComplete(middleMatrix);
	}

	/**
	 * Sets the right side of the middle matrix to be the merged side of the
	 * left side of the right matrix with the right side of the middle matrix
	 * 
	 * @param rightMatrix
	 *            the matrix on the right
	 * @param middleMatrix
	 *            the matrix in the middle
	 */
	private void addRightSide(int[][] rightMatrix, int[][] middleMatrix) {
		int[] leftSide = getLeftSide(rightMatrix);
		int[] rightSide = getRightSide(middleMatrix);
		int[] mergedSide = mergeSides(rightSide, leftSide);
		setRightSide(middleMatrix, mergedSide);
	}

	/**
	 * Sets the left side of the middle matrix to be the merged side of the
	 * right side of the left matrix with the left side of the middle matrix
	 * 
	 * @param leftmatrix
	 *            the matrix on the left
	 * @param middleMatrix
	 *            the matrix in the middle
	 */
	private void addLeftSide(int[][] leftmatrix, int[][] middleMatrix) {
		int[] rightSide = getRightSide(leftmatrix);
		int[] leftSide = getLeftSide(middleMatrix);
		int[] mergedSide = mergeSides(leftSide, rightSide);
		setLeftSide(middleMatrix, mergedSide);
	}

	/**
	 * Sets the bottom side of the middle matrix to be the merged side of the
	 * top side of the bottom matrix with the bottom side of the middle matrix
	 * 
	 * @param bottomMatrix
	 *            the matrix at the bottom
	 * @param middleMatrix
	 *            the matrix in the middle
	 */
	private void addBottomSide(int[][] bottomMatrix, int[][] middleMatrix) {
		int[] bottomRow = getBottomRow(middleMatrix);
		int[] topRow = getTopRow(bottomMatrix);
		int[] mergedSide = mergeSides(bottomRow, topRow);
		setBottomSide(middleMatrix, mergedSide);
	}

	/**
	 * Sets the top side of the middle matrix to be the merged side of the
	 * bottom side of the top matrix with the top side of the middle matrix
	 * 
	 * @param topMatrix
	 *            the matrix at the top
	 * @param middleMatrix
	 *            the matrix in the middle
	 */
	private void addTopSide(int[][] topMatrix, int[][] middleMatrix) {
		int[] bottomRow = getBottomRow(topMatrix);
		int[] topRow = getTopRow(middleMatrix);
		int[] mergedSide = mergeSides(bottomRow, topRow);
		setTopSide(middleMatrix, mergedSide);
	}

	/**
	 * Set the top side of the middle matrix to be the new merged side
	 * 
	 * @param middleMatrix
	 *            the matrix in the middle
	 * @param mergedSide
	 *            the merged side of the middle with the top matrix
	 */
	private void setTopSide(int[][] middleMatrix, int[] mergedSide) {
		MatrixUtils.setFirstRow(middleMatrix, mergedSide);
	}

	/**
	 * Set the bottom side of the middle matrix to be the new merged side
	 * 
	 * @param middleMatrix
	 *            the matrix in the middle
	 * @param mergedSide
	 *            the merged side of the middle with the bottom matrix
	 */
	private void setBottomSide(int[][] middleMatrix, int[] mergedSide) {
		MatrixUtils.setLastRow(middleMatrix, mergedSide);
	}

	/**
	 * Set the left side of the middle matrix to be the new merged side
	 * 
	 * @param middleMatrix
	 *            the matrix in the middle
	 * @param mergedSide
	 *            the merged side of the middle with the left matrix
	 */
	private void setLeftSide(int[][] middleMatrix, int[] mergedSide) {
		MatrixUtils.setFirstColumn(middleMatrix, mergedSide);
	}

	/**
	 * Set the right side of the middle matrix to be the new merged side
	 * 
	 * @param middleMatrix
	 *            the matrix in the middle
	 * @param mergedSide
	 *            the merged side of the middle with the right matrix
	 */
	private void setRightSide(int[][] middleMatrix, int[] mergedSide) {
		MatrixUtils.setLastColumn(middleMatrix, mergedSide);
	}

	// <--- start of setters & getters for matrix sides --->

	/**
	 * 
	 * @param matrix
	 * @return
	 */
	private int[] getTopRow(int[][] matrix) {
		return MatrixUtils.getTopRow(matrix);
	}

	/**
	 * 
	 * @param matrix
	 * @return
	 */
	private int[] getBottomRow(int[][] matrix) {
		return MatrixUtils.getBottomRow(matrix);
	}

	/**
	 * 
	 * @param middle
	 * @return
	 */
	private int[][] getMiddleMatrix(PuzzlePiece middle) {
		return MatrixUtils.deepCopy(middle.getMatrix());
	}

	/**
	 * 
	 * @param side1
	 * @param side2
	 * @return
	 */
	private static int[] mergeSides(int[] side1, int[] side2) {
		return MatrixUtils.mergeMatrices(side1, side2);
	}

	/**
	 * 
	 * @param matrix
	 * @return
	 */
	private int[] getLeftSide(int[][] matrix) {
		return MatrixUtils.getFirstColumn(matrix);
	}

	/**
	 * 
	 * @param matrix
	 * @return
	 */
	private int[] getRightSide(int[][] matrix) {
		return MatrixUtils.getLastColumn(matrix);
	}

	// >--- end of setters & getters for matrix sides ---<
}
